前天我們介紹了Flux framework 套件 Redux,昨天實作 Redux 和 React 做串接,裡面提到不少概念:
react-redux 就可以方便把 Redux 和 React 串接今天要談的是最後也最重要的東西: Redux Middleware
所有的範例程式放在 ithelp-30dayfullstack-hello-redux,有需要的請自取。
學習完 middleware,我想當你在套用 Redux 相關套件 react-router-redux, connected-react-router、redux-thunk、redux-observable、redux-saga…等會有很大的信心和理解在做什麼。
Redux 也有 middleware,它類似 express middleware,我不得不說他更能難讓人理解,因為它是 higher-order function。不管理解到什麼程度,只要記得一件事:
Redux middleware 內的實作要把 action 送到 next(action) 中
Redux 在沒有引入 middleware 前的運作都是同步的,引入非同步的 middleware 就可以派分出非同步的 action。Redux Middleware 的威力很強,讓第三方的程式嵌入 Redux 的運作中。
Redux Middleware 和 Express Middleware 有一點相似之處:
res, req, next 換到下一個 res, req, next middleware ,res、req 都是物件,呼叫 next() 換下一個 middleware 執行。dispatch 產生一個新的 dispatch,dispatch 是(處理 action)函數,在它的實作中要把 action 送到 next(action) 中, next 是內層的 dispatch function。文件是用 Flow notation定義,我也列出 JSDoc 定義,選你習慣的看
type Action = Object
type AsyncAction = any
type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }
type BaseDispatch = (a: Action) => Action
type Dispatch = (a: Action | AsyncAction) => any
type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch
Action: object (含有 type 屬性)
AsyncAction: any
MiddlewareAPI: { dispatch: Dispatch, getState: () => State }
function BaseDispatch(a: Action): Action
function Dispatch(a: Action | AsyncAction): any
function Middleware(api: MiddlewareAPI): function(next: Dispatch): Dispatch
上面的 BaseDispatch 一般是指最內層的 dispatch function,會把 action 送到 reducer 中。由 Redux 提供,就是之前提的 store.dispatch。然而,一個 middleware 的作用,會在 store.dispatch 外層在套一個 dispatch function,類似:
const oldDispatch = store.dispatch // 留下舊的 dispatch function
const newDispatch = action => {
  return oldDispatch(action);
}
store.dispatch = newDispatch; // 替換成新的 dispatch function
用 middleware 來演示就像:
const oldDispatch = store.dispatch // 留下舊的 dispatch function
const middleware = dispatch => {
  const newDispatch = action => {
    return oldDispatch(action);
  } 
  return newDispatch;
}
store.dispatch = middleware(oldDispatch); // 替換成新的 dispatch function
以上就是 redux middleware 期望要做的事。
接下來,我們仔細的看 redux middleware 的簽章。 Middleware 會輸入一個 api 參數,回傳一個函數。 分別看它們的型態:
MiddlewareAPI。它是一個物件,有 dispatch, getState 屬性。原始碼 表示只為了把做出包含 api 的閉包。function(next: Dispatch): Dispatch,送入 Dispatch 回傳 Dispatch,所以用箭頭函數寫實作就要出現型如:
next => {
  return <Dispatch>
}
<Deispatch> 打開
next => {
  return (action) => {
    return <any>
  }
}
next 其實就是內層的 dispatch function。為了方便理解 middleware 我們先不要管 api,就直接看真的要實作的 middleware(即作用完 api 的回傳函數)。
考慮兩個 middleware F,G 和 印出資料的 baseDispatch,這裡 F,G的簽章都是 function(next: Dispatch): Dispatch 所以它們可以串接,有兩種串法 F ● G 或 G ● F,我們只考慮 F ● G 合成,我可以得到最後的 dispatch function:
dispatchFG = F(G(baseDispatch));
若把 action 送到 dispatchFG,就是
dispatchFG(action) = F(G(baseDispatch))(action)
令 dispatchF = F(G(baseDispatch))
F(G(baseDispatch))(action) = dispatchF(action)
就是說 action 會先被 dispatchF 作用。
令dispatchG = G(baseDispatch)
得到
F(G(baseDispatch))(action) = F(dispatchG)(action)
Redux 要求:F 內的實作也要把 action 送到 dispatchG(action) 中 。
若把 dispatchG 改叫 next 重寫上面一句話: F 內的實作也要把 action 送到 next(action) 中 。
因此,就得到重要的規則:
Redux middleware 內的實作要把 action 送到 next(action) 中
這就和 express middleware 要求:
Express middleware 內的實作一定要呼叫 next() 一樣
// mimicBasic.js
/**
 * 
 * @callback Dispatch
 * @param {Action} action
 * @returns {any}
 */
/**
 * 
 * @param {Dispatch} next 
 * @returns {Dispatch}
 */
function F(next) {
  return function dispatchF(action) {
    console.log('dispatchF');
    action = action + ` -> F`;
    next(action); // next = dispatchG = G(baseDispatch)
  };
}
/**
 * 
 * @param {Dispatch} next 
 * @returns {Dispatch}
 */
function G(next) {
  return function dispatchG(action) {
    console.log('dispatchG');
    // action
    action = action + ` -> G`;
    next(action); // next = baseDispatch
  };
}
/**
 * 
 * @type {Dispatch} 
 */
function baseDispatch(action) {
  console.log(action);
}
/**
 * 合成 middleware F, G
 * @type {Dispatch} 
 */
const dispatchFG = F(G(baseDispatch));
dispatchFG('action');
console.log('done');
結果:
dispatchF
dispatchG
action -> F -> G
done
圖解就是如下:
(C) 是才是範例的圖,(B) 是只看 G 的作用,(A) 是沒有任何 middleware 的圖。(你可能需要花時間自己想圖的作用)
還原成 Redux 原來的定義,把 api 弄進來,最後再寫成箭頭函數,就是 Redux middleware 最完整的簽章。
// mimic.js
const f = (api) => next => action => {
  console.log('dispatchF');
  action = action + ` -> F`;
  next(action); // next = g(api)(baseDispatch)
};
const g = (api) => next => action => {
  console.log('dispatchG');
  action = action + ` -> G`;
  next(action); // next = baseDispatch
};
const baseDispatch = (action) => console.log(action);
function applayMiddleware(f, g) {
  const api = {};
  const G =  g(api);
  const F = f(api);
  const dispatchG = G(baseDispatch);
  const dispatchFG = F(dispatchG);
  return dispatchFG; // F(G(baseDispatch)) = f(api)(g(api)(baseDispatch))
}
const dispatchFG = applayMiddleware(f, g);
dispatchFG('action');
console.log('done');
Redux middleware 一定長成 (api: MiddlewareAPI) => (next: Dispatch) => Dispatch,才可以做合成  F ● G。
我們觀察到:
next 是指內層的 dispatch function,因為 (F ● G(BaseDispatch))(action) = F(dispatchG)(action) = F(next)(action)
next(action),action 才能一直往內層送,不然就會斷掉,reducer 就收不到 action 了。action => action,其它中間過程是 action 被改成長什麼樣子都可以,所以你才會看到 AsyncAction: any 這特別的定義。return next(action) 在非同步的 middleware 比較少用。前面我們分析了 redux middleware,看不懂沒關係可能是我寫的不好 ><。
只要記得一件事:
Redux middleware 內的實作要把 action 送到 next(action) 中
這樣才能引起一連串的內部 dispatch function 運作。剩下的只要知道 middleware 簽章,你也可以寫出自己的 middleware。
接下來,我們來套用 redux middleware 到 store 中,只需要 createStore() 中使用 applyMiddleware()。
當沒有用任何 middleware 時,像
const store = createStore(reducer, initState);
此時的 store 的 dispatch function store.dispatch 是「某個 BaseDispatch 的實體」(我們暫時稱為 aBaseDispatch),這是最內層的 dispatch function,內部會把 action 送入 reducer 中。
假如,套用 logger middleware,
function logger({ getState }) {
  return next => action => {
    console.log('will dispatch: ' + JSON.stringify(action));
    // 送 action 到內層 dispatch function
    const returnValue = next(action);
    console.log('state after dispatch: ' + JSON.stringify( getState()));
    // 在同步的 middleware 才有用  
    return returnValue;
  }
}
const { createStore, applyMiddleware } = require('redux');
const store = createStore(reducer, initState, applyMiddleware(logger));
把 middleware 用到 store dispatch function 中,要用 applyMiddleware 這函數。 applyMiddleware(...middleware) 會回傳 enhancer,給 createStore 使用 (就是 enhancer(createStore) 變成新的 store)。
此時 store 的 dispatch function store.dispatch 的真實身份是 logger(aBaseDispatch),
若送出一個 action,
const action = {
  type: identityChangeMessage,
  payload: {
    message: 'change',
  },
}
action 經過 logger middleare 會印出
will dispatch: {"type":"CHANGE_MESSAGE","payload":{"message":"change"}}
state after dispatch: {"message":"change"}
這裡雖然是自己做非同步 middleware 但只是學習用,除非你有獨到的見解或其它考量,否則還是建議使用 middleware 套件:redux-thunk、redux-observable、redux-saga。
假設我們可以發出一個帶有 promise 的 action,例如:
const promiseAction = {
  type: 'CHANGE_MESSAGE',
  promise: Promise.resolve({message: 'changed'})
}
store.dispatch(promiseAction);
PromiseMiddleware: 可以處理 pomise 的 middleware我們需一個 middleware,它要做以下的事
return next(action))_TRIGGER, _SUCCESS, _FAIL)
CHANGE_MESSAGE_TRIGGER
CHANGE_MESSAGE_SUCCESS,且 resolve data 放在 action.payload
CHANGE_MESSAGE_FAIL,且 reject error 放在 action.error
next(action) 讓內層 dispatch fuction 作用把上述寫成程式
// PromiseMiddleware
function PromiseMiddleware(action) {
  return next => {
    return function dispatchAsync(action) {
      if (action.promise instanceof Promise) {
        console.log('Promise action');
        const { type, promise, ...others } = action;
        promise
          .then(data => {
            next({
              type: success(type),
              payload: data,
              promise,
              ...others
            });
          })
          .catch(error => {
            next({
              type: fail(type),
              error: error,
              promise,
              ...others
            });
          });
        return next({
          type: trigger(type),
          promise,
          ...others
        });
      } else {
        console.log('Not promise action');
        return next(action);
      }
    }
  }
}
修改一下之面的 logger 方便我們觀察 action
function logger({ getState }) {
  return next => action => {
    console.log('========== action dispatching(start) ===============');
    console.log('will dispatch: ' + JSON.stringify(action));
    const returnValue = next(action);
    console.log('state after dispatch: ' + JSON.stringify(getState()));
    console.log('========== action dispatching(end) ===============');
    return returnValue;
  }
}
PromiseMiddleware 和 logger middlewares接下來,使用 PromiseMiddleware 和 logger
const store = createStore(reducer, initState, applyMiddleware(PromiseMiddleware, logger));
要小心,middleware 順序不能換,因為 promise action 要先進到 PromiseMiddleware 的 dispatch function(dispatchAsync) 作用, action 才能進到內層的 logger 中 dispatch function 印出。
你也可以試試看順序倒過來會怎麼樣。
定義一個 promise action
const identityChangeMessage
// Case 1: 建立一個 resolve action
const resolvePromiseAction = {
  type: identityChangeMessage,
  promise: Promise.resolve({
    message: 'changed',
  })
};
派分 promise action
store.dispatch(resolvePromiseAction);
結果如下:
========== action dispatching(start) ===============
will dispatch: {"type":"CHANGE_MESSAGE_TRIGGER","promise":{}}
state after dispatch: {"message":"identityChangeMessage trigger"}
========== action dispatching(end) ===============
waiting...
========== action dispatching(start) ===============
will dispatch: {"type":"CHANGE_MESSAGE_SUCCESS","payload":{"message":"changed"},"promise":{}}
state after dispatch: {"message":"changed"}
========== action dispatching(end) ===============
我們發現產生了兩個 action,action type 分別是 CHANGE_MESSAGE_TRIGGER, CHANGE_MESSAGE_SUCCESS,這也符合 Promise 的運作過程,就像是我們模擬 Redux 版本的 fetch() request。當派分 promise action 後,在 reducer 就要收到一個 _TRIGGER 的 action,然後取回資料後,reducer 就要收到一個 _SUCCESS 的 action。
reject promise action 和 normal action 的完整的範例見 middlewareAsync.js
PromiseMiddleware非同步的 middleware 有下列的特性:
PromiseMiddleware 中解讀 promise action, 就可以產生其它 action 來模擬 Promise 的運作過程。next(action),就像我們發出了二個 actions 
實務上,在發出 某 action 後可能又要發出其它 action,如下圖
雖然這大量產生的 action 讓人有點詬病,但使用良好的非同步 action 套件,可以一定程度控制我們的程式碼,避免程式碼混亂。
next(action) 中的 action 可以在 middleware 中任意建立、修改、更換,例如:我們建立新的 action {type: success(type),  payload: data, promise, ...others} ( action type 加入後綴詞)Redux 是很小的套件,以它為核心已經發展出大量的相關套件,當然你想要什麼功能除了自己實作,也可以用別人的套件。
我的學習方法是先自己試寫看看體驗一下痛苦,再套用大神們的套件,因為自己臨時寫的 API 我不覺得會比大神們經過時間粹煉的套件好用、穩定。
Redux 生態系列表如下:
我還是可以小小的註解一下:
redux-thunk:我第一個用的非同步 middleware 套件。它的原始碼很簡單,把 action 結構改成一個函數(物件),然後派分函數 action。我覺得 action 連發、維護不太好處理,我就轉為 redux-observable。
redux-observable:要用 reactive programming 的概念,RxJS 是 javascript 的 reactive programming 實現套件。 redux-observable 把它們引入 Redux 中。
redux-saga:使用 ES6 的生成器函式(generator function)/ yield 語法不用學新的語法。
react-router 是 react compoent 套件,用來依照網址選擇要渲染的 component。它是 React 相關套件,它與 redux 沒關西。
因為篇福有限,我只點出一件很重要的事: 使用 react-router 要小心版本號
若你要用 react-router 4.x 請用以下組合:
若你要用 react-router 2.x and 3.x 請用以下組合:
今天主要介紹 Redux Middleware,並分別給出了同步和非同步的 middleware 的範例 logger 和 PromiseMiddleware,並在建立 store 時使用 middleware,最後以Redux 生態系為結尾。